using System;
using System.Data.Entity;
using System.Data.Entity.Validation;
using System.Linq;
using Nop.Core;
using Nop.Core.Data;
using System.Data.Common;
using System.Data;
using Nop.Core.Log;

namespace Nop.Data
{
    /// <summary>
    /// Entity Framework repository
    /// </summary>
    public partial class EfRepository<T> : IRepository<T> where T : BaseEntity
    {
        private readonly IDbContext _context;
        private IDbSet<T> _entities;

        /// <summary>
        /// Ctor
        /// </summary>
        /// <param name="context">Object context</param>
        public EfRepository(IDbContext context)
        {
            this._context = context;
        }

        /// <summary>
        /// Get entity by identifier
        /// </summary>
        /// <param name="id">Identifier</param>
        /// <returns>Entity</returns>
        public virtual T GetById(object id)
        {
            ResetReadableDatabaseConnection();

            //see some suggested performance optimization (not tested)
            //http://stackoverflow.com/questions/11686225/dbset-find-method-ridiculously-slow-compared-to-singleordefault-on-id/11688189#comment34876113_11688189
            return this.Entities.Find(id);
        }

        /// <summary>
        /// Insert entity
        /// </summary>
        /// <param name="entity">Entity</param>
        public virtual void Insert(T entity)
        {
            try
            {
                SetWritableDatabaseConnection();

                if (entity == null)
                    throw new ArgumentNullException("entity");

                this.Entities.Add(entity);

                this._context.SaveChanges();
            }
            catch (DbEntityValidationException dbEx)
            {
                var msg = string.Empty;

                foreach (var validationErrors in dbEx.EntityValidationErrors)
                    foreach (var validationError in validationErrors.ValidationErrors)
                        msg += string.Format("Property: {0} Error: {1}", validationError.PropertyName, validationError.ErrorMessage) + Environment.NewLine;

                var fail = new Exception(msg, dbEx);
                //Debug.WriteLine(fail.Message, fail);
                throw fail;
            }
        }

        /// <summary>
        /// Update entity
        /// </summary>
        /// <param name="entity">Entity</param>
        public virtual void Update(T entity)
        {
            try
            {
                SetWritableDatabaseConnection();
                if (entity == null)
                    throw new ArgumentNullException("entity");
                var entry = (this._context as DbContext).Entry(entity);
                if(entity!=null)
                    entry.State = EntityState.Modified;                
				this._context.SaveChanges();
            }
            catch (DbEntityValidationException dbEx)
            {
                var msg = string.Empty;

                foreach (var validationErrors in dbEx.EntityValidationErrors)
                    foreach (var validationError in validationErrors.ValidationErrors)
                        msg += Environment.NewLine +
                               string.Format("Property: {0} Error: {1}", validationError.PropertyName,
                                   validationError.ErrorMessage);

                var fail = new Exception(msg, dbEx);
                //Debug.WriteLine(fail.Message, fail);
                throw fail;
            }
        }

        /// <summary>
        /// Delete entity
        /// </summary>
        /// <param name="entity">Entity</param>
        public virtual void Delete(T entity)
        {
            try
            {
                SetWritableDatabaseConnection();

                if (entity == null)
                    throw new ArgumentNullException("entity");

                this.Entities.Remove(entity);

                this._context.SaveChanges();
            }
            catch (DbEntityValidationException dbEx)
            {
                var msg = string.Empty;

                foreach (var validationErrors in dbEx.EntityValidationErrors)
                    foreach (var validationError in validationErrors.ValidationErrors)
                        msg += Environment.NewLine + string.Format("Property: {0} Error: {1}", validationError.PropertyName, validationError.ErrorMessage);

                var fail = new Exception(msg, dbEx);
                //Debug.WriteLine(fail.Message, fail);
                throw fail;
            }
        }


		public virtual void UseMasterDB()
		{ 			
			SetWritableDatabaseConnection();			
		}

        /// <summary>
        /// Gets a table
        /// </summary>
        public virtual IQueryable<T> Table
        {
            get
            {
                return this.Entities;
            }
        }


        /// <summary>
        /// Gets a table with "no tracking" enabled (EF feature) Use it only when you load record(s) only for read-only operations
        /// </summary>
        public virtual IQueryable<T> TableNoTracking
        {
            get
            {
                return this.Entities.AsNoTracking();
            }
        }


        /// <summary>
        /// Entities
        /// </summary>
        protected virtual IDbSet<T> Entities
        {
            get
            {
                if (_entities == null)
                    _entities = _context.Set<T>();
                return _entities;
            }
        }

        private void SetWritableDatabaseConnection()
        {
            string cs = SQLDispatcherConfiguration.WritableDB.ConnectionString;
            UpdateConnectionStringIfNeed((_context as DbContext).Database.Connection, cs);
        }

        private void ResetReadableDatabaseConnection()
        {
            string region = SQLDispatchContext.GetCurrentContext().Region;

            string cs = GetReadableConnectionString(region);

            UpdateConnectionStringIfNeed((_context as DbContext).Database.Connection, cs);
        }

        private static string GetReadableConnectionString(string region)
        {

            if (region == null || region.Length == 0)       //from normal read dbs
            {
                int random = GenerateRandomNumber();
                int dbIndex = random % SQLDispatcherConfiguration.ReadDBs.Count;
                return SQLDispatcherConfiguration.ReadDBs[dbIndex].ConnectionString;
            }

            DedicatedRegion r = SQLDispatcherConfiguration.DedicatedRegions.Find(t => t.Region.Equals(region, StringComparison.OrdinalIgnoreCase));
            if (r == null)
                throw new Exception("No such Dedicated Region Identifier.");

            {
                int random = GenerateRandomNumber();
                int dbIndex = random % r.DBs.Count;
                return r.DBs[dbIndex].ConnectionString;
            }
        }

        private static int GenerateRandomNumber()
        {
            Random Random1 = new Random();
            int i1 = Random1.Next(0, 1001);
            return i1;
        }

        private static void UpdateConnectionStringIfNeed(DbConnection conn, string connectionString)
        {
            if (!ConnectionStringCompare(conn, connectionString))
            {
                ConnectionState state = conn.State;
                if (state == ConnectionState.Open)
                    conn.Close();

                conn.ConnectionString = connectionString;

                if (state == ConnectionState.Open)
                    conn.Open();
            }
        }

        private static bool ConnectionStringCompare(DbConnection conn, string connectionString)
        {
            DbProviderFactory factory = DbProviderFactories.GetFactory(conn);

            DbConnectionStringBuilder a = factory.CreateConnectionStringBuilder();
            a.ConnectionString = conn.ConnectionString;

            DbConnectionStringBuilder b = factory.CreateConnectionStringBuilder();
            b.ConnectionString = connectionString;

            return a.EquivalentTo(b);
        }
    }
}